﻿using System;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;

namespace oueb.react
{
    using oueb.Ceen;
    using oueb.react.help;
    using SanlieHash;
    using SanlieHash.Fast;
    using System.IO.Compression;

    public class ReactWebModule : IHttpModule
    {
        internal string default_file = "index.html";
        internal const string IMG_FAVICON = "favicon.ico";
        internal const string FILE_ROBOTS = "robots.txt";
        internal const string FILE_MANIFEST = "manifest.json";
        internal const string gzipcache_ext = ".ouebgz";
        internal ConcurrentDictionary<string, CacheBag<DateTime>> filelasttimes = new ConcurrentDictionary<string, CacheBag<DateTime>>();
        internal string path { get; set; }
        internal string path_cache { get; set; }
        public int CacheDays = 3650;
        public int Fileinfo_CacheSenconds = 60;
        public bool GzipMode = true;
        internal List<string> NoCacheFiles { get; set; }
        internal bool status_cache = true;
        internal ReactWebModule(string path, string cachepath="cache")
        {
            this.path = path;
            this.path_cache = cachepath;
            if (!Directory.Exists(cachepath)) Directory.CreateDirectory(cachepath);
            NoCacheFiles = [default_file, FILE_MANIFEST, IMG_FAVICON, FILE_ROBOTS];
        }
        internal bool NO_CACHE(string _path)
        {
            foreach (var _p in this.NoCacheFiles) if (_path.EndsWith(_p)) return true;
            return false;
        }
        public void SetDefaultFile(string filename) { this.default_file = filename; }
        public void DisableCache() { this.status_cache = false; }
        public void EnableCache() { this.status_cache = true; }
        public void NoCacheEndWith(string pathend)
        {
            if (this.NoCacheFiles.Contains(pathend)) return;
            this.NoCacheFiles.Add(pathend);
        }
        public void NoCacheEndWithNot(string pathend)
        {
            if (! this.NoCacheFiles.Contains(pathend)) return;
            this.NoCacheFiles.Remove(pathend);
        }
        public static ReactWebModule Open(string path)
        {
            return new ReactWebModule(path);
        }
        public async Task<bool> HandleAsync(IHttpContext context)
        {
            var _path = context.Request.Path;
            _path = Path.Join(path, _path);
            if (File.Exists(_path))
            {
                if (helper.IsOutOfScope(_path, path)) return false;
                await SendFile(context, _path);
                return true;
            }
            else if (Directory.Exists(_path))
            {
                _path = Path.Join(_path, default_file);
                if (helper.IsOutOfScope(_path, path)) return false;
                if (File.Exists(_path)) await SendFile(context, _path);
                return true;
            }

            if (_path.IndexOf('.') != -1) return false;

            if (File.Exists(_path + ".htm")) await SendFile(context, _path + ".htm");
            if (File.Exists(_path + ".html")) await SendFile(context, _path + ".html");

            _path = Path.Join(path, default_file);
            if (File.Exists(_path)) await SendFile(context, _path);
            return true;

        }
        protected readonly Regex ETAG_RE = new Regex(@"\s*(?<isWeak>W\\)?""(?<etag>\w+)""\s*,?");
        internal DateTime readlastmodified(string tag, string _path)
        {
            DateTime lastmodified;
            var now = DateTime.UtcNow;
        CHECK_CACHE:
            if (filelasttimes.TryGetValue(tag, out var _cache))
            {
                if (_cache.CacheTime.AddSeconds(Fileinfo_CacheSenconds) >= now)
                {
                    lastmodified = _cache.Bag;
                    goto NEXT;
                }
            }
            else
            {
                _cache = new CacheBag<DateTime> { CacheTime = now };
                if (!filelasttimes.TryAdd(tag, _cache)) goto CHECK_CACHE;
            }
            lastmodified = File.GetLastWriteTimeUtc(_path);
            _cache.Bag = lastmodified;

        NEXT:
            return lastmodified;
        }
        internal Task SendFile(IHttpContext context, string _path)
        {
            if (MimeTypes.Try(_path, out var mimetype)) context.Response.ContentType = mimetype;

            var tag = FastHash.GetSM3(_path);

            var lastmodified = readlastmodified(tag, _path);
            var etag = tag + FastEncode.EncodeHex(BitConverter.GetBytes(lastmodified.ToBinary()));
            if (!status_cache) goto SEND_FILE;
            if (NO_CACHE(_path))
            {
                DateTime modifiedsincedate;
                DateTime.TryParseExact(context.Request.Headers["If-Modified-Since"], CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out modifiedsincedate);

                var ce = ETAG_RE.Matches(context.Request.Headers["If-None-Match"] ?? string.Empty);
                if (ce.Count > 0)
                {
                    var clientetags = new string[ce.Count];
                    for (var i = 0; i < clientetags.Length; i++)
                    {
                        clientetags[i] = ce[i].Groups["etag"].Value;
                        if (etag != null && string.Equals(clientetags[i], etag, StringComparison.OrdinalIgnoreCase))
                            goto HL_304;
                    }
                }

                if (modifiedsincedate == lastmodified) goto HL_304;
                context.Response.AddHeader("Last-Modified", lastmodified.ToString("R", CultureInfo.InvariantCulture));
                context.Response.SetExpires(TimeSpan.FromDays(CacheDays));
                context.Response.Headers["Cache-Control"] += ", no-cache";
                context.Response.Headers["ETag"] = $"\"{etag}\"";
                goto SEND_FILE;
            HL_304:
                if (etag != null)
                {
                    SetNotModified(context, etag);
                    return Task.CompletedTask;
                }
            }
            else
            {
                context.Response.SetExpires(TimeSpan.FromDays(CacheDays));
                context.Response.Headers["Cache-Control"] += ", immutable";
            }

            SEND_FILE:
            if (GzipMode)
            {
                if (context.Request.Headers.ContainsKey("Range")) return _send_file(context, _path);
                context.Response.GzipOn();
                if (etag != null) return _send_file_byetag(context, _path, etag);
            }
            return _send_file(context, _path);
        }
        internal async Task _send_file_byetag(IHttpContext context, string _path, string etag)
        {
            var cachepath = Path.Join(path_cache, $"{etag}{gzipcache_ext}");

            if (File.Exists(cachepath)) goto SEND_GZIP;

            using (var fs = File.Create(cachepath))
            using (var fs_org = File.Open(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            using (var sr = new GZipStream(fs, CompressionLevel.Fastest, true)) fs_org.CopyTo(sr);

            SEND_GZIP:
            using (var file = File.Open(cachepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                await context.Response.WriteStreamBy206Partial(file);
        }
        internal async Task _send_file(IHttpContext context, string _path)
        {
            using (var file = File.Open(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                await context.Response.WriteStreamBy206Partial(file);
        }
        private bool SetNotModified(IHttpContext context, string etag)
        {
            context.Response.StatusCode = HttpStatusCode.NotModified;
            context.Response.ContentLength = 0;
            context.Response.SetExpires(TimeSpan.FromDays(CacheDays));
            if (!string.IsNullOrWhiteSpace(etag))
                context.Response.Headers["ETag"] = $"\"{etag}\"";
            return true;
        }

    }
}
